import java.io.IOException;
import java.util.HashMap;
import java.util.ArrayList;


public class Driver {

	HashMap<String,Object> variables;
	boolean debug;
	Integer depth;
	
	public static void main(String[] args) throws IOException
	{
		Driver driver = new Driver();
		
		Scanner s = new Scanner();
        //read in the script
		s.scan(args[0]);
		MiniReRDP rdp = new MiniReRDP(s);
        //trim away the non-functional nodes
        rdp.getNode().trim();

        //for debugging purposes

        //driver.setDebug(true);
		//rdp.getNode().printTree();
		//System.out.println(rdp.getNode().toString());

		System.out.println("Start The Program:");
		System.out.println();

        //start running the program
        driver.statement_list(rdp.getNode());
	}

	public Driver() {
		variables = new HashMap<String,Object>();
		debug = false;
		depth = 0;
	}
	
	public void setDebug(boolean b) {
		debug = b;
	}

    public void statement_list(Node root)
    {
    	if (debug) System.out.println(generateSpaces(depth++) + "entering statement_list");
        
        if(root.getChildren().size() == 1){
            //this if statement is for robust exceution, if the tree is malformed this will fix it
            if(root.get(0).getToken() == null){
                statement_list(root.get(0));
            }else{
                //in this case there is only one statement in the script
                statement(root.get(0));
            }
        }else if(root.getChildren().size() == 2){
            //execute the current statement
            statement(root.get(0));
            //recurse
            statement_list(root.get(1));
        }
    	if (debug) System.out.println(generateSpaces(--depth) + "exiting statement_list");
    	if (debug) printVariables();
    }

    //this function just detects each case and executing it
    public void statement(Node n)
    {
    	if (debug) System.out.println(generateSpaces(depth++) + "entering statement with:" + n);
        if(n.getToken().getType() == TokenClass.EQUALS){
            equals(n);
        }else if(n.getToken().getType() == TokenClass.HASH){
            hash(n);
        }else if(n.getToken().getType() == TokenClass.MAXFREQSTR){
            maxFreqStr(n);
        }else if(n.getToken().getType() == TokenClass.PRINT){
            print(n);
        }else if(n.getToken().getType() == TokenClass.REPLACE){
            replace(n);
        }else if(n.getToken().getType() == TokenClass.RECURSIVEREPLACE){
            recursiveReplace(n);
        }else {
			System.out.println("Failed processing a statement with node:\n" + n);
			printVariables();
			System.exit(1);
		}
        
    	if (debug) System.out.println(generateSpaces(--depth) + "exiting statement");
    }

    public void equals(Node n)
    {
    	if (debug) System.out.println(generateSpaces(depth++) + "entering equals");

        //simply put the string for the ID into the hashmap with the value of the expression that follows
        variables.put(n.get(0).getToken().getLiteral(),exp(n.get(1)));

    	if (debug) System.out.println(generateSpaces(--depth) + "exiting equals");
    }

    public Object exp(Node n)
    {

// debug & error checking

    	if (debug) System.out.println(generateSpaces(depth++) + "entering exp at Node:" + n);
        //sanity check
		if( n.getChildren().size() > 2 ) {
			System.out.println("failed executing exp; node has >2 children");
			printVariables();
			System.exit(1);
		}
// ending debug & error checking

//start actual code.

        //check for the first case, it is simply an ID
        //must check for null first, else we risk a null pointer exception
        if(n.getToken() != null  && n.getToken().getType() == TokenClass.ID){
            if (debug) System.out.println(generateSpaces(--depth) + "exiting exp");
            //return whatever is in variables
            //if the script does not first define the ID then we will get an error
            return variables.get(n.getLiteral());
        }else{
            //if the size is one then it must be a term
            if(n.size() == 1){
                if (debug) System.out.println(generateSpaces(--depth) + "exiting exp");
                return term(n.get(0));
            //in the last case there is a series of finds and bin ops
            }else if(n.size() == 2){
                if (debug) System.out.println("expression case 2");
                if (debug) System.out.println(generateSpaces(--depth) + "exiting exp");
                return exp_tail(term(n.get(0)),n.get(1)); 
            }
        }
        //this should never execute
        System.out.println("exp was not given the correct node.");
        System.exit(1);
        return "WTF SOMETHING BROKE";
    }

    //this will take in a list of SMOs for ease of executing the bin_ops
    public ArrayList<StringMatchObject> exp_tail(ArrayList<StringMatchObject> list, Node n)
    {
    	if (debug) System.out.println(generateSpaces(depth++) + "entering exp_tail with:\n" + n);
        //the union case
        if(n.getType() == TokenClass.UNION){
            if (debug) System.out.println(generateSpaces(--depth) + "exiting exp_tail");
            n = n.get(0);
            //we have reached the end of the statement, start returning
            if(n.size() == 1) return bin_union(list,term(n.get(0)));
            //there is more in the statment, keep on recursing
            return exp_tail(bin_union(list,term(n.get(0))),n.get(1));
        }

        //the intersection case
        if(n.getType() == TokenClass.INTERS){
            if (debug) System.out.println(generateSpaces(--depth) + "exiting exp_tail");
            n = n.get(0);
            //we have reached the end of the statement, start returning
            if(n.size() == 1) return bin_inter(list,term(n.get(0)));
            //there is more in the statment, keep on recursing
            return exp_tail(bin_inter(list,term(n.get(0))),n.get(1));
        }

        //the diff case
        if(n.getType() == TokenClass.DIFF){
            if (debug) System.out.println(generateSpaces(--depth) + "exiting exp_tail");

            n = n.get(0);
            //we have reached the end of the statement, start returning
            if(n.size() == 1) return bin_diff(list,term(n.get(0)));
            //there is more in the statment, keep on recursing
            return exp_tail(bin_diff(list,term(n.get(0))),n.get(1));
        }       
        System.out.println("failed attempting to execute at exp_tail");
        System.out.println("Current tree:" + n);
        System.out.println("Variables:" + variables);
		printVariables();
        System.exit(1);
        return new ArrayList<StringMatchObject>();
    }

    public ArrayList<StringMatchObject> term(Node n){
    	if (debug) System.out.println(generateSpaces(depth++) + "entering term");
		//check for find node
		if( n.getType() != TokenClass.FIND ) {
                System.out.println("execution failed in term: not a find node");
                System.out.println("Current node:" + n);
                printVariables();
                System.exit(1);
        }
        //create the regex matcher
        Regex regex = new Regex(n.get(0).getToken().getLiteral());
    	if (debug) System.out.println(generateSpaces(--depth) + "exiting term");
        if (debug) regex.setDebug(debug);
        //run the regex and return it
        return regex.find(n.get(1).getToken().getLiteral());
	}
        
        
	private void hash( Node n){
    	if (debug) System.out.println(generateSpaces(depth++) + "entering hash with:" + n);
        //evaluate the expression of the right side
        ArrayList<StringMatchObject> exp =  (ArrayList<StringMatchObject>) exp(n.get(1));
        //simply put the size of the list of SMOs into the variables.
        //this will break if it is run on an ID that has an Integer
        variables.put(n.get(0).getLiteral(),exp.size());
	}


	private void maxFreqStr( Node n) {
    	if (debug) System.out.println(generateSpaces(depth++) + "entering maxfreqstr");
        Integer max = 0;
        //get the ID that is referenced, if it is not declared as a list of SMOs it will crash and burn
        for(StringMatchObject i: (ArrayList<StringMatchObject>) variables.get(n.get(1).getLiteral())){
            if(i.getLocations().size() > max){
                max = i.getLocations().size();
                //every time we hit a new maximum we will put the variable in. This is inefficient.
                variables.put(n.get(0).getLiteral(),i);
            }
        }
    	if (debug) System.out.println(generateSpaces(--depth) + "exiting maxfreqstr");
    }


    private void replace( Node n) {
    	if (debug) System.out.println(generateSpaces(depth++) + "entering replace");
        //create the regex
        Regex regex = new Regex(n.get(0).getLiteral());
        //run the regex
        regex.replace(n.get(2).get(0).getLiteral(),n.get(2).get(1).getLiteral(),n.get(1).getLiteral());
        //debug
    	if (debug) System.out.println(generateSpaces(--depth) + "exiting replace");
    }


	private void recursiveReplace( Node n ) {
    	if (debug) System.out.println(generateSpaces(depth++) + "entering recursivereplace");
        //check to see if there is an error in the tree
		if( n.getChildren().size()!=3 ) {
			System.out.println("execution failed in recursive replace, not exactly 3 children.");
			printVariables();
			System.exit(1);
		}
        //make the regex
        Regex regex = new Regex(n.get(0).getLiteral());
        //run the regex
        regex.recursiveReplace(n.get(2).get(0).getLiteral(),n.get(2).get(1).getLiteral(),n.get(1).getLiteral());
        //debug
    	if (debug) System.out.println(generateSpaces(--depth) + "exiting recursivereplace");
    }


	private void print(Node n) {
    	if (debug) System.out.println(generateSpaces(depth++) + "entering print");
        //get and print the list of expressions thats refrenced, will call the arrayList's to string
        System.out.println(exp_list(n.get(0)));
    	if (debug) System.out.println(generateSpaces(--depth) + "exiting print");
	}

    //can return a list of both SMOs and Integers
    private ArrayList<Object> exp_list( Node n){
        ArrayList<Object> list = new ArrayList<Object>();
    	if (debug) System.out.println(generateSpaces(depth++) + "entering exp_list at Node " + n);
        //only contains one expression, just return it
        if(n.size() == 1){
            list.add(exp(n.get(0)));
        //contains more than one expression
        }else{
            list.add(exp(n.get(0)));
            //recurse
            list.addAll(exp_list(n.get(1)));
        }
    	if (debug) System.out.println(generateSpaces(--depth) + "exiting exp_list");
        return list;
    }
            
        

	private ArrayList<StringMatchObject> bin_diff( ArrayList<StringMatchObject> lhs, ArrayList<StringMatchObject> rhs ) {
    	if (debug) System.out.println(generateSpaces(depth++) + "entering bin_diff");
        //to use so we do not remove objects from a list while iterating over it
        ArrayList<StringMatchObject> removeList = new ArrayList<StringMatchObject>();
        //iterate over both finding the cases where they have a literal thats the same
		for( StringMatchObject li: lhs ) {
			for( StringMatchObject ri: rhs) {
				if( li.getLiteral().equals(ri.getLiteral()) ) {
                    removeList.add(li);
				}
			}
		}

        //remove the objects now, no concurrent modifications!
         for(StringMatchObject i : removeList){
                lhs.remove(i);
        }
        
    	if (debug) System.out.println(generateSpaces(--depth) + "exiting bin_diff");
		return lhs;
	}

	private ArrayList<StringMatchObject> bin_union ( ArrayList<StringMatchObject> lhs, ArrayList<StringMatchObject> rhs ) {
        
        //handle if both are empty
        if(lhs.size() == 0 && rhs.size() == 0) return new ArrayList<StringMatchObject>();
        //handle if one of them is empty
        if(lhs.size() == 0) return rhs;
        if(rhs.size() == 0) return lhs;
        
    	if (debug) System.out.println(generateSpaces(depth++) + "entering bin_union with sets:" + lhs + "\n and:" + rhs);
		ArrayList<StringMatchObject> ret_val = new ArrayList<StringMatchObject>();
		boolean center = false;
        //to avoid concurrent modifaction errors
        ArrayList<StringMatchObject> removeList = new ArrayList<StringMatchObject>();

		for( StringMatchObject li: lhs) {
			for( StringMatchObject ri: rhs) {
				if( li.getLiteral().equals(ri.getLiteral()) ) {
                    //union the metadata
                    li.addLocations(ri.getLocations());
                    //to avoid concurrent modifaction errors
                    removeList.add(ri);
                }
            }
        }

        //to avoid concurrent modifaction errors
        for(StringMatchObject i : removeList){
                rhs.remove(i);
        }

        //iterate over adding all of them to the return variable
		for( StringMatchObject li: lhs) {
			for( StringMatchObject ri: rhs) {
				if( li.getLiteral().equals(ri.getLiteral()) ) {
					ret_val.add( new StringMatchObject( li.getLiteral(), li.getLocations(), ri.getLocations() ));
					center = true;
				}
				else {
					ret_val.add( new StringMatchObject( ri ));
				}
			}
			if( !center ) {
				ret_val.add( new StringMatchObject( li ) ) ;
				center = false;
			}
		}

    	if (debug) System.out.println(generateSpaces(--depth) + "exiting bin_union");
		return ret_val;
	}
	

	private ArrayList<StringMatchObject> bin_inter ( ArrayList<StringMatchObject> lhs, ArrayList<StringMatchObject> rhs ) {

    	if (debug) System.out.println(generateSpaces(depth++) + "entering bin_inter");
		ArrayList<StringMatchObject> ret_val = new ArrayList<StringMatchObject>();

        //iterate over both lists
		for( StringMatchObject li: lhs ) {
			for( StringMatchObject ri: rhs ) {
				if( li.getLiteral().equals(ri.getLiteral()) ) {
                    //add the ones we found existing in both, with both of their metadatas
					ret_val.add( new StringMatchObject( li.getLiteral(), li.getLocations(), ri.getLocations()));		
				}
			}
		}
    	if (debug) System.out.println(generateSpaces(--depth) + "exiting bin_inter");
		return ret_val;
	}

    //used for debugging purposes
	private void printVariables() {
		System.out.println("Variables table:");
		for( Object i: variables.keySet() )
			System.out.println( i + "\t\t" + variables.get(i) );
	}

	private String generateSpaces(Integer n) {
		String out = "";
		for (int i=0; i<n; i++) out += " ";
		return out;
	}
}
